package services

import (
	"bytes"
	"context"
	"fmt"
	"gitee.com/igolang/imoney/internal/app/system/entities"
	"gitee.com/igolang/imoney/internal/app/system/entities/constants"
	"gitee.com/igolang/imoney/internal/app/system/keys"
	"gitee.com/igolang/imoney/internal/app/system/services/dict"
	"gitee.com/igolang/imoney/internal/app/system/tools"
	commonConstants "gitee.com/igolang/imoney/internal/common/constants"
	commonEntities "gitee.com/igolang/imoney/internal/common/entities"
	"gitee.com/igolang/imoney/internal/common/httpResp"
	commonTools "gitee.com/igolang/imoney/internal/common/tools"
	"gitee.com/igolang/imoney/internal/core/client"
	"gitee.com/igolang/imoney/internal/models/dao"
	"gitee.com/igolang/imoney/internal/models/model"
	"gitee.com/igolang/imoney/internal/router/middleware"
	"gitee.com/igolang/pkg/xgin"
	"gitee.com/igolang/pkg/xjson"
	"gitee.com/igolang/pkg/xtime"
	"gitee.com/igolang/pkg/xutils"
	"github.com/gin-gonic/gin"
	"github.com/mileusna/useragent"
	"github.com/spf13/cast"
	"gorm.io/gen"
	"net/http"
	"strings"
	"time"
)

var (
	OperLog    = &operLog{}
	Logininfor = &logininfor{}
)

type (
	operLog    struct{}
	logininfor struct{}
)

func (serv *logininfor) List(ctx *gin.Context) (*commonEntities.ListResp, error) {
	var req entities.LogininforPageListReq
	if err := ctx.ShouldBind(&req); err != nil {
		return nil, err
	}
	req.Page()
	conds := serv.formatListConds(req)
	total, err := dao.SysLogininfor.Where(conds...).Count()
	if err != nil {
		return nil, err
	}
	result := &commonEntities.ListResp{
		Total: total,
		Rows:  make([]*model.SysLogininfor, 0),
	}
	if total == 0 {
		return result, nil
	}
	result.Rows, err = dao.SysLogininfor.Where(conds...).Limit(req.PageSize).Offset(req.Offset).Order(dao.SysLogininfor.InfoID.Desc()).Find()
	return result, err
}

func (serv *logininfor) AddUserLoginLog(ctx *gin.Context, req *entities.LoginUserReq, result *entities.LoginResult) {
	ip := xutils.ClientIp(ctx.Request)
	ua := useragent.Parse(ctx.GetHeader("User-Agent"))
	loc := xutils.Ip2Location(ip)
	t := time.Now()
	row := &model.SysLogininfor{
		UserName:      req.Username,
		Ipaddr:        ip,
		LoginLocation: loc,
		Browser:       ua.Name,
		Os:            ua.OS,
		Status:        result.Status,
		Msg:           result.Msg,
		LoginTime:     xtime.Time(t),
	}

	if dao.SysLogininfor.Create(row) == nil && result.T == "login" && result.Status == constants.OkStatus {
		// 写入用户在线信息
		tokenKey := keys.UserToken(result.Id)
		client.Redis().HMSet(
			context.TODO(),
			tokenKey.Key,
			"browser", ua.Name,
			"ipaddr", ip,
			"loginLocation", loc,
			"loginTime", t.Unix(),
			"os", ua.OS,
		)
	}
}

func (serv *logininfor) Delete(ctx *gin.Context) (err error) {
	ids := commonTools.String2Int64s(ctx.Param("ids"))
	if len(ids) == 0 {
		return httpResp.ErrorCode(httpResp.ErrCodeInvalidParams)
	}

	_, err = dao.SysLogininfor.Where(dao.SysLogininfor.InfoID.In(ids...)).Delete()
	return
}

func (serv *logininfor) Clean() (err error) {
	dc := client.DB()
	return dc.Exec(`TRUNCATE TABLE ` + dao.SysLogininfor.TableName()).Error
}

func (serv *logininfor) Export(ctx *gin.Context) (b *bytes.Buffer, err error) {
	var req entities.LogininforPageListReq
	if err = ctx.ShouldBind(&req); err != nil {
		return nil, err
	}
	conds := serv.formatListConds(req)
	rows, err := dao.SysLogininfor.Where(conds...).Rows()
	if err != nil {
		return
	}

	header := []string{"序号", "用户账号", "登录状态", "登录地址", "登录地点", "浏览器", "操作系统", "提示消息", "访问时间"}
	datas := [][]string{header}
	dc := client.DB()
	commonStatusOptions, _ := dict.Get("sys_common_status")
	for rows.Next() {
		row := &model.SysLogininfor{}
		if e := dc.ScanRows(rows, row); e != nil {
			continue
		}
		datas = append(datas, []string{
			cast.ToString(row.InfoID),
			row.UserName,
			dict.LabelOptions(commonStatusOptions, row.Status),
			row.Ipaddr,
			row.LoginLocation,
			row.Browser,
			row.Os,
			row.Msg,
			row.LoginTime.String(),
		})
	}
	fh := xutils.CreateExcelFileWithData(datas)
	defer fh.Close()
	b, err = fh.WriteToBuffer()
	return
}

func (serv *logininfor) formatListConds(req entities.LogininforPageListReq) []gen.Condition {
	conds := make([]gen.Condition, 0)
	req.Page()
	if req.Ipaddr != "" {
		conds = append(conds, dao.SysLogininfor.Ipaddr.Eq(req.Ipaddr))
	}
	if req.UserName != "" {
		conds = append(conds, dao.SysLogininfor.UserName.Eq(req.UserName))
	}
	if req.Status != "" {
		conds = append(conds, dao.SysLogininfor.Status.Eq(req.Status))
	}

	if !req.BT.IsZero() && !req.ET.IsZero() {
		conds = append(conds, dao.SysLogininfor.LoginTime.Gte(xtime.Time(req.BT)), dao.SysLogininfor.LoginTime.Lte(xtime.Time(req.ET)))
	}

	return conds
}

func (serv *operLog) List(ctx *gin.Context) (*commonEntities.ListResp, error) {
	var req entities.OperLogPageListReq
	if err := ctx.ShouldBind(&req); err != nil {
		return nil, err
	}
	req.Page()
	conds := serv.formatListConds(req)
	count, err := dao.SysOperLog.Where(conds...).Count()
	if err != nil {
		return nil, err
	}
	result := &commonEntities.ListResp{
		Total: count,
		Rows:  make([]*model.SysOperLog, 0),
	}
	if count == 0 {
		return result, nil
	}
	offset := (req.PageNum - 1) * req.PageSize
	result.Rows, err = dao.SysOperLog.Where(conds...).Limit(req.PageSize).Offset(offset).Order(dao.SysOperLog.OperID.Desc()).Find()

	return result, err
}

func (serv *operLog) Export(ctx *gin.Context) (b *bytes.Buffer, err error) {
	var req entities.OperLogPageListReq
	if err = ctx.ShouldBind(&req); err != nil {
		return
	}
	conds := serv.formatListConds(req)
	rows, err := dao.SysOperLog.Where(conds...).Rows()
	if err != nil {
		return
	}
	header := []string{"操作序号", "操作模块", "业务类型", "请求方法", "请求方式", "操作类别", "操作人员", "部门名称", "请求地址", "操作地址", "操作地点", "请求参数", "返回参数", "状态", "错误消息", "操作时间", "消耗时间"}
	datas := [][]string{header}
	dc := client.DB()
	operTypeOptions, _ := dict.Get("sys_oper_type")
	commonStatusOptions, _ := dict.Get("sys_common_status")
	for rows.Next() {
		row := &model.SysOperLog{}
		if e := dc.ScanRows(rows, row); e != nil {
			continue
		}
		datas = append(datas, []string{
			cast.ToString(row.OperID),
			row.Title,
			dict.LabelOptions(operTypeOptions, cast.ToString(row.BusinessType)),
			row.Method,
			row.RequestMethod,
			serv.valueToLabel("OperatorType", row.OperatorType),
			row.OperName,
			row.DeptName,
			row.OperURL,
			row.OperIP,
			row.OperLocation,
			row.OperParam,
			row.JSONResult,
			dict.LabelOptions(commonStatusOptions, cast.ToString(row.Status)),
			row.ErrorMsg,
			row.OperTime.String(),
			fmt.Sprintf("%d毫秒", row.CostTime),
		})

	}
	fh := xutils.CreateExcelFileWithData(datas)
	defer fh.Close()
	b, err = fh.WriteToBuffer()
	return
}

func (serv *operLog) valueToLabel(t string, value int64) string {
	operatorTypeMap := map[int64]string{
		0: "其他",
		1: "后台用户",
		2: "手机端用户",
	}
	if t == "OperatorType" {
		return operatorTypeMap[value]
	}

	statusMap := map[int64]string{
		0: "正常",
		1: "异常",
	}

	if t == "Status" {
		return statusMap[value]
	}

	return ""
}

func (serv *operLog) formatListConds(req entities.OperLogPageListReq) []gen.Condition {
	conds := make([]gen.Condition, 0)
	req.Page()
	if req.Title != "" {
		conds = append(conds, dao.SysOperLog.Title.Like(`%`+req.Title+`%`))
	}
	if req.OperName != "" {
		conds = append(conds, dao.SysOperLog.OperName.Like(`%`+req.OperName+`%`))
	}
	if req.BusinessType != "" {
		conds = append(conds, dao.SysOperLog.BusinessType.Eq(cast.ToInt64(req.BusinessType)))
	}
	if req.Status != "" {
		conds = append(conds, dao.SysOperLog.Status.Eq(cast.ToInt64(req.Status)))
	}
	if !req.BT.IsZero() && !req.ET.IsZero() {
		conds = append(conds, dao.SysOperLog.OperTime.Gte(xtime.Time(req.BT)), dao.SysOperLog.OperTime.Lte(xtime.Time(req.ET)))
	}
	return conds
}

func (serv *operLog) Clean() (err error) {
	dc := client.DB()
	return dc.Exec(`TRUNCATE TABLE ` + dao.SysOperLog.TableName()).Error
}

func (serv *operLog) Delete(ctx *gin.Context) (err error) {
	ids := commonTools.String2Int64s(ctx.Param("ids"))
	if len(ids) == 0 {
		return httpResp.ErrorCode(httpResp.ErrCodeInvalidParams)
	}
	_, err = dao.SysOperLog.Where(dao.SysOperLog.OperID.In(ids...)).Delete()
	return
}

// AddOperationLog
// 添加操作日志，类型，create update delete import export
// 类型判断从path中获取
func (serv *operLog) AddOperationLog(ctx *gin.Context, path string, resp *bytes.Buffer) {
	routeInfo := xgin.Route(path, ctx.Request.Method)
	if routeInfo == nil {
		return
	}
	var bType constants.OperationLogType
	per, _ := routeInfo.Ext.(*entities.RoutePerm)
	if per != nil {
		bType = per.OperationLogType
	}
	if bType == 0 {
		return
	}

	xutils.RecoverGo(func() {
		userIfo := tools.GetUserInfo(ctx)
		var result string
		isExport := false
		r := &httpResp.Resp{}
		if bType == constants.OperationLogExport {
			if ctx.Writer.Header().Get(commonConstants.ExportHeaderRespKey) != commonConstants.ExportHeaderRespValue {
				xjson.Unmarshal(resp.Bytes(), r)
				result = resp.String()
			} else {
				r.Code = http.StatusOK
				result = "导出数据"
			}

		} else {
			xjson.Unmarshal(resp.Bytes(), r)
			result = resp.String()
		}

		reqParams := ctx.GetStringMap(middleware.REQUEST)
		b, _ := xjson.Marshal(reqParams)
		ip := xutils.ClientIp(ctx.Request)
		// 获取耗时
		t := ctx.GetTime(commonConstants.RouterMiddleRequestTimeKey)
		since := time.Since(t).Milliseconds()
		row := &model.SysOperLog{
			Title:         routeInfo.Name,
			BusinessType:  int64(bType),
			Method:        routeInfo.HandlerName(),
			RequestMethod: ctx.Request.Method,
			OperatorType:  1,
			OperName:      userIfo.User.UserName,
			OperURL:       path,
			OperIP:        ip,
			OperLocation:  xutils.Ip2Location(ip),
			OperParam:     bytes.NewBuffer(b).String(),
			JSONResult:    result,
			ErrorMsg:      r.Message,
			OperTime:      xtime.Time(time.Now()),
			CostTime:      since,
			DeptName:      userIfo.User.Dept.DeptName,
		}

		if r.Code != http.StatusOK {
			row.Status = 1
		}

		if isExport {
			row.BusinessType = 5
			if r.Code == 0 {
				row.Status = 0
			}
		}

		dao.SysOperLog.Create(row)
	}, serv.addErrHandler("OperationLog"))
}

func (serv *operLog) getBusinessType(method, path string) int {
	m := map[string]int{
		"other":   0,
		"/upload": 0,
		"/create": 1,
		"/add":    1,
		"/update": 2,
		"/remove": 3,
		"/delete": 3,
		"/import": 4,
		"/export": 5,
		"/clean":  9,
	}

	for k, v := range m {
		if strings.Contains(path, k) {
			return v
		}
	}

	methodMap := map[string]int{
		http.MethodPost:   1,
		http.MethodPut:    2,
		http.MethodDelete: 3,
	}

	return methodMap[strings.ToUpper(method)]
}

func (serv *operLog) addErrHandler(action string) func(err interface{}) {
	return commonTools.ErrorHandler(context.Background(), "Add %s Log recover err", action)
}
